# -*- coding: utf-8 -*-
import json
import logging
import re
from datetime import datetime
from decimal import Decimal
from hashlib import md5
from sqlalchemy import distinct

from future.utils import raise_with_traceback

from common import orm
from common.account.model.account import (Account, AccountToken, ACCOUNT_STATUS, BANK_LIST, FinancialAccount,
                                          ADMIN_SEARCHED_STATUS, BALANCE_TYPE)
from common.cache import redis_cache
from common.cache.bull_cache import check_banker_lock, get_lock_amount, BULL_LOCK_RATIO
from common.pay.model import Pay, PAY_STATUS
from common.transaction.db import create_sharding_transaction, get_sharding_transaction_table
from common.transaction.model import TRANSACTION_TYPE, TRANSACTION_STATUS
from common.utils import EnhencedEncoder
from common.utils import exceptions as err
from common.utils import id_generator
from common.utils import tracker
from common.utils.citizen_id import verify_citizen_id
from common.utils.currency import convert_yuan_to_fen, convert_fen_to_yuan
from common.utils.decorator import sql_wrapper
from common.utils.exceptions import FinancialAccountError
from common.utils.respcode import StatusCode
from common.utils.db import paginate, get_orderby, get_count
from common.utils.helper import paginate
from common.black_account.db import get_withdraw_black_list, update_withdraw_black, create_withdraw_black_modification
from common.black_account.model import WITHDRAW_BLACK_STATUS, WITHDRAW_BLACK_TYPE
from common.utils.api import mask_string

_LOGGER = logging.getLogger(__name__)
_SALT = u"V7$#g*BN"


def encode_password(passwd):
    return md5(passwd.encode('utf-8') + _SALT).hexdigest()


@sql_wrapper
def get_account_by_username(user_name):
    account = Account.query.filter(Account.user_name == user_name).first()
    return account


@sql_wrapper
def get_account_by_phone(phone):
    account = Account.query.filter(Account.phone == phone).first()
    return account


@sql_wrapper
def get_protected_user_name(user_id):
    account = Account.query.filter(Account.id == user_id).first()
    user_name = account.user_name
    return u'%s***%s' % (user_name[0], user_name[-1])


def get_account(user_id, use_cache=False):
    if use_cache:
        account_dict = redis_cache.get_account(user_id)
        if not account_dict:
            account = orm.session.query(Account).filter(Account.id == user_id).first()
            cache_value = json.dumps(account.as_dict(),
                                     cls=EnhencedEncoder, ensure_ascii=False)
            redis_cache.save_account(user_id, cache_value)
        else:
            account = Account()
            account.from_dict(json.loads(account_dict))
    else:
        account = orm.session.query(Account).filter(Account.id == user_id).first()
    return account


def get_user_name(user_id):
    account = orm.session.query(Account).filter(Account.id == user_id).first()
    if account:
        return account.user_name


def get_user_id_by_username(user_name):
    account = Account.query.filter(Account.user_name == user_name).first()
    if account:
        return account.id


@sql_wrapper
def get_account_in_ids(ids):
    if not ids:
        return []
    accounts = Account.query.filter(Account.id.in_(ids)).all()
    return accounts


@sql_wrapper
def get_logon_info(user_id):
    info = AccountToken.query.filter(AccountToken.user_id == user_id).order_by(
        AccountToken.created_at.desc()).first()
    if info:
        data = json.loads(info.extend or '{}')
        data.update({'token': info.token})
        return data
    else:
        return {}


@sql_wrapper
def exists_phone(phone_num):
    account = Account.query.filter(Account.phone == phone_num).first()
    return account


@sql_wrapper
def exists_username(username):
    account = Account.query.filter(Account.user_name == username).first()
    return account


@sql_wrapper
def create_account(info, extend=None):
    account = Account()
    if 'user_name' in info:
        account.user_name = info['user_name']
    if 'phone' in info:
        account.phone = info['phone']
        account.status = ACCOUNT_STATUS.PHONED
        account.user_name = info['phone']
    account.password = encode_password(info['password'])
    account.extend = json.dumps(extend) if extend else None
    for k in 'avatar', 'channel', 'is_virtual':
        setattr(account, k, info.get(k))
    account.save()
    return account


@sql_wrapper
def delete_account(user_id):
    Account.query.filter(Account.id == user_id).update({
        'deleted': 1,
        'updated_at': datetime.utcnow()
    })
    orm.session.commit()


@sql_wrapper
def black_account(user_id, admin_id, reason=''):
    """
    拉黑账户
    """
    account = Account.query.with_for_update().filter(
        Account.id == user_id).one()
    account.status = ACCOUNT_STATUS.BANNED
    if reason:
        extend = json.loads(account.extend or '{}')
        extend['blackfor'] = reason
        account.extend = json.dumps(extend)
    account.save(auto_commit=False)
    orm.session.save()
    tracker.track_black(user_id, reason)


@sql_wrapper
def get_black_account(query_dct):
    account_id = query_dct.get("account_id", None)
    status = query_dct.get("status", None)
    withdraw_status = query_dct.get("withdraw_status")
    bind_account_type = query_dct.get('fin_account_type')
    if account_id is not None:
        return orm.session.query(Account).filter(Account.id == account_id).all(), 1
    else:
        query = orm.session.query(Account)
        if status is not None or withdraw_status is not None:
            black_lists = get_withdraw_black_list()
            if status is not None:
                status = int(status)
                test_uids = black_lists.get(WITHDRAW_BLACK_TYPE.TEST_UID, [])
                test_uids = [int(test_uid) for test_uid in test_uids]
                sales_uids = black_lists.get(WITHDRAW_BLACK_TYPE.BLACK_SALES, [])
                sales_uids = [int(sale_uid) for sale_uid in sales_uids]
                op_uids = black_lists.get(WITHDRAW_BLACK_TYPE.BLACK_OPERATIONS, [])
                op_uids = [int(op_uid) for op_uid in op_uids]
                if status == ADMIN_SEARCHED_STATUS.NORMAL:
                    query = query.filter(~Account.id.in_(test_uids + sales_uids + op_uids)).filter(
                        Account.status != ACCOUNT_STATUS.BANNED)
                elif status == ADMIN_SEARCHED_STATUS.TEST:
                    query = query.filter(Account.id.in_(test_uids))
                elif status == ADMIN_SEARCHED_STATUS.SALES:
                    query = query.filter(Account.id.in_(sales_uids))
                elif status == ADMIN_SEARCHED_STATUS.OPERATION:
                    query = query.filter(Account.id.in_(op_uids))
                elif status == ADMIN_SEARCHED_STATUS.BLACK:
                    query = query.filter(Account.status == ACCOUNT_STATUS.BANNED)
            if withdraw_status is not None:
                black_uids = black_lists.get('black_uid', [])
                black_uids = [int(black_uid) for black_uid in black_uids]
                if int(withdraw_status):
                    query = query.filter(Account.id.in_(black_uids))
                elif not int(withdraw_status):
                    query = query.filter(~Account.id.in_(black_uids))
        if bind_account_type:
            if bind_account_type == 'phone':
                query = query.filter(Account.status == ACCOUNT_STATUS.PHONED)
            else:
                query = query.filter(Account.id == FinancialAccount.user_id,
                                     FinancialAccount.type == bind_account_type)
        total_count = get_count(query)
        orderby = get_orderby(query_dct.get('$orderby'), Account)
        if orderby is not None:
            query = query.order_by(orderby)
        query = paginate(query, query_dct)
        return query.all(), total_count


@sql_wrapper
def set_black_status(account_id, operator_user, reason, status, admin_id):
    account = Account.query.with_for_update().filter(Account.id == account_id).one()
    extend = json.loads(account.extend or '{}')
    mod_status = None
    if 'old_status' not in extend:
        extend['old_status'] = '0'
    if "BLACK" == status and account.status != ACCOUNT_STATUS.BANNED:
        extend['old_status'] = account.status
        account.status = ACCOUNT_STATUS.BANNED
        mod_status = WITHDRAW_BLACK_STATUS.ENABLE
    elif "UNBLACK" == status and extend['old_status'] != ACCOUNT_STATUS.BANNED:
        old_status = account.status
        account.status = extend['old_status']
        extend['old_status'] = old_status
        mod_status = WITHDRAW_BLACK_STATUS.INVALID
    extend['blackfor'] = reason
    extend['black_operator'] = operator_user
    extend['black_time'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
    account.extend = json.dumps(extend)
    account.save(auto_commit=False)
    if mod_status:
        create_withdraw_black_modification(account.id, admin_id, "account_banned", account.id, mod_status, reason)
    orm.session.commit()


@sql_wrapper
def get_account_by_chn_ip(query_dct):
    aid = query_dct.get("aid", None)
    ip = query_dct.get("ip", None)
    last_login_ip = query_dct.get("last_login_ip", None)
    query = None
    total_count = None
    if aid:
        query = Account.query.filter(Account.extend.like('%' + aid + '%'))
    if ip:
        if query is None:
            query = Account.query.filter(Account.extend.like('%' + ip + '%'))
        else:
            query = query.filter(Account.extend.like('%' + ip + '%'))
    if query is None and last_login_ip:
        # query = AccountToken.query.filter(AccountToken.extend.like('%' + last_login_ip + '%')).distinct(AccountToken.user_id)
        query = orm.session.query(distinct(AccountToken.user_id)).filter(
            AccountToken.extend.like('%' + last_login_ip + '%'))
    if query:
        total_count = get_count(query)
        query = paginate(query, query_dct)
    if query:
        return query.all(), total_count
    else:
        return query, 0


@sql_wrapper
def reset_phone(user_id, admin_id, reason=''):
    """
    解绑手机号
    """
    account = Account.query.with_for_update().filter(
        Account.id == int(user_id)).first()
    phone = account.phone
    account.phone = None
    account.status = account.status - ACCOUNT_STATUS.PHONED
    info = {'type_account': phone, 'type': WITHDRAW_BLACK_TYPE.BLACK_PHONE, 'note': u'解绑手机号',
            'money': '0', 'reason': reason}
    update_withdraw_black(info, admin_id, WITHDRAW_BLACK_STATUS.ENABLE)
    account.save(auto_commit=False)
    orm.session.commit()


@sql_wrapper
def update_charged_status(user_id):
    """
    更新用戶狀態
    """
    account = Account.query.with_for_update().filter(
        Account.id == user_id).one()
    account.status |= ACCOUNT_STATUS.CHARGED
    account.save()


@sql_wrapper
def get_charged_status(user_id):
    """
    讀取用戶狀態
    """
    account = Account.query.with_for_update().filter(
        Account.id == user_id).one()
    return account.status & ACCOUNT_STATUS.CHARGED


@sql_wrapper
def update_account(user_id, info):
    account = Account.query.with_for_update().filter(
        Account.id == user_id).one()
    if 'name' in info:  # 实名认证仅能更新一次
        name = info.pop('name')
        citizen_id = info.pop('citizen_id')
        if verify_citizen_id(citizen_id):
            account.name = name
            account.citizen_id = citizen_id
            account.status |= ACCOUNT_STATUS.NAMED
        else:
            raise err.DataError('citizen_id is illegal')
    if 'trade_pwd' in info:
        account.status |= ACCOUNT_STATUS.TRADEABLE

    if 'phone' in info:
        account.status |= ACCOUNT_STATUS.PHONED

    for k, v in info.iteritems():
        setattr(account, k, v)
    account.save()

    cache_value = json.dumps(
        account.as_dict(), cls=EnhencedEncoder, ensure_ascii=False)
    redis_cache.save_account(user_id, cache_value)
    return account


@sql_wrapper
def reset_passwd(phone, password):
    user = Account.query.with_for_update().filter(
        Account.phone == phone).first()
    if not user:
        raise err.DataError(status=StatusCode.INVALID_USER)
    user.password = encode_password(password)
    user.save()
    return user.id


@sql_wrapper
def login_account(user_name, password, chn='', cvc='', extend=None, login_account=False):
    user = Account.query.filter(
        Account.user_name == user_name).first()
    if not user:
        raise_with_traceback(err.AuthenticateError(
            status=StatusCode.INVALID_USER))
    if user.status & ACCOUNT_STATUS.BANNED:
        raise err.PermissionError('forbidden')

    if login_account is False and encode_password(password) != user.password:
        raise_with_traceback(err.AuthenticateError(
            status=StatusCode.WRONG_USERNAME_PASSWORD))
    token = id_generator.generate_uuid()
    record = AccountToken()
    record.user_id = user.id
    record.token = token
    record.chn = chn
    record.cvc = cvc
    record.extend = json.dumps(extend) if extend else None
    record.deleted = 0
    record.save()

    data = user.as_dict()
    data['token'] = token
    return data


@sql_wrapper
def login_account_with_phone(user_name, password, chn="", cvc="", extend=None):
    password = encode_password(password)
    user = None
    if re.match(r'\d{11}', user_name):
        phone = '86' + user_name
        user = Account.query.filter(Account.phone == phone) \
            .filter(Account.password == password).first()
    if not user:
        user = Account.query.filter(Account.user_name == user_name) \
            .filter(Account.password == password).first()
    if not user:
        raise_with_traceback(err.AuthenticateError(
            status=StatusCode.WRONG_USERNAME_PASSWORD))
    if user.status & ACCOUNT_STATUS.BANNED:
        raise err.PermissionError('forbidden')

    token = id_generator.generate_uuid()
    record = AccountToken()
    record.user_id = user.id
    record.token = token
    record.chn = chn
    record.cvc = cvc
    record.extend = json.dumps(extend) if extend else None
    record.deleted = 0
    record.save()

    data = user.as_dict()
    data['token'] = token
    return data


@sql_wrapper
def login_by_username(user_name, password, chn="", cvc="", extend=None):
    password = encode_password(password)
    user = Account.query.filter(Account.user_name == user_name) \
        .filter(Account.password == password).first()
    if not user:
        raise_with_traceback(err.AuthenticateError(
            status=StatusCode.WRONG_USERNAME_PASSWORD))
    if user.status & ACCOUNT_STATUS.BANNED:
        raise err.PermissionError('forbidden')

    token = id_generator.generate_uuid()
    record = AccountToken()
    record.user_id = user.id
    record.token = token
    record.chn = chn
    record.cvc = cvc
    record.extend = json.dumps(extend) if extend else None
    record.deleted = 0
    record.save()

    data = user.as_dict()
    data['token'] = token
    return data


@sql_wrapper
def login_by_phone(phone, password, chn="", cvc="", extend=None):
    password = encode_password(password)
    phone = '86' + phone
    user = Account.query.filter(Account.phone == phone) \
        .filter(Account.password == password).first()
    if not user:
        raise_with_traceback(err.AuthenticateError(
            status=StatusCode.WRONG_PHONE_PASSWORD))
    if user.status & ACCOUNT_STATUS.BANNED:
        raise err.PermissionError('forbidden')

    token = id_generator.generate_uuid()
    record = AccountToken()
    record.user_id = user.id
    record.token = token
    record.chn = chn
    record.cvc = cvc
    record.extend = json.dumps(extend) if extend else None
    record.deleted = 0
    record.save()

    data = user.as_dict()
    data['token'] = token
    return data


@sql_wrapper
def logout_device(user_id, token):
    AccountToken.query.filter(AccountToken.user_id == user_id).filter(
        AccountToken.token == token).update({
        'deleted': 1,
        'updated_at': datetime.utcnow()
    })
    orm.session.commit()


@sql_wrapper
def logout_user(user_id):
    AccountToken.query.filter(AccountToken.user_id == user_id).update({
        'deleted': 1,
        'updated_at': datetime.utcnow()
    })
    orm.session.commit()


@sql_wrapper
def get_online_info(user_id, token=None):
    if token:
        return AccountToken.query.filter(
            AccountToken.user_id == user_id).filter(
            AccountToken.token == token).first()
    else:
        return AccountToken.query.filter(
            AccountToken.user_id == user_id).filter(
            AccountToken.deleted == 0).order_by(
            AccountToken.created_at.desc()).first()


def get_banks():
    return BANK_LIST


@sql_wrapper
def check_trade_pwd(user_id, trade_pwd):
    account = Account.query.filter(
        Account.id == user_id).one()
    return account.trade_pwd == encode_password(trade_pwd)


def add_campaign_award_in_transaction(user_id, added_balance, extend):
    """
    活动活动奖励
    """
    _LOGGER.info('add account balance, user_id[%s] balance[%s]' % (user_id, add_account_balance))
    account = Account.query.filter(Account.id == user_id).with_lockmode('update').first()
    account.balance += added_balance
    account.save(auto_commit=False)

    trans = create_sharding_transaction(user_id, {'type': TRANSACTION_TYPE.AWARD,
                                                  'title': extend.get('title'),
                                                  'extend': json.dumps(extend),
                                                  'price': added_balance,
                                                  'balance': account.balance,
                                                  'status': TRANSACTION_STATUS.DONE,
                                                  })
    orm.session.flush()
    return trans.id


@sql_wrapper
def add_account_balance(user_id, pay_id, added_balance, extend):
    """
    充值
    """
    _LOGGER.info(
        'add account balance, user_id[%s] balance[%s]' % (
            user_id, added_balance))
    # update pay record status
    res = Pay.query.filter(Pay.id == pay_id).filter(
        Pay.status == PAY_STATUS.SUBMIT).update({
        'status': PAY_STATUS.DONE,
        'price': added_balance,
        'updated_at': datetime.utcnow()
    })
    if res:
        account = Account.query.filter(
            Account.id == user_id).with_lockmode('update').first()
        account.balance = account.balance + \
                          (0 if not added_balance else Decimal(added_balance))
        account.save(auto_commit=False)
        # add or modify transaction
        transaction_table = get_sharding_transaction_table(user_id)
        trans = transaction_table.query.filter(transaction_table.pay_id == pay_id).first()
        if not trans:
            trans = create_sharding_transaction(user_id, {'pay_id': pay_id,
                                                          'type': TRANSACTION_TYPE.IN,
                                                          })
        trans.title = extend.get('title')
        trans.extend = json.dumps(extend.get('ext'), ensure_ascii=False)
        trans.price = added_balance
        trans.balance = account.balance
        trans.status = TRANSACTION_STATUS.DONE
        trans.save(auto_commit=False)
        orm.session.commit()
    else:
        _LOGGER.warn(
            'add account balance, cocurrency occured!, pay_id[%s]' % pay_id)


def decrease_balance_in_transaction(user_id, amount):
    assert amount >= 0
    account = Account.query.with_for_update().filter(
        Account.id == user_id).one()
    if check_banker_lock(user_id):
        raise err.LockError('bull banker lock')
    if account.balance < max(amount, BULL_LOCK_RATIO * Decimal(get_lock_amount(user_id))):
        raise err.BalanceInsufficient('account balance insufficient')
    account.balance = Decimal(account.balance) - Decimal(amount)
    account.withdraw = min(account.balance, account.withdraw)
    return account


def get_user_settings(user):
    settings = json.loads(user.settings or '{}')
    return {
        'reward_push': settings.get('reward_push', True),
        'reward_animation': settings.get('reward_animation', True)
    }


def update_account_settings(user, user_settings):
    old_settings = json.loads(user.settings or '{}')
    old_settings.update(user_settings)
    user.settings = json.dumps(old_settings, ensure_ascii=False)
    user.save()


def set_financial_account(user_id, account, account_type, bank, sub_bank, real_name):
    financial_account = FinancialAccount.query.filter(FinancialAccount.user_id == user_id). \
        filter(FinancialAccount.type == account_type).first()
    if financial_account:
        raise FinancialAccountError(u'提现账号已绑定')
    financial_account = FinancialAccount()
    financial_account.user_id = user_id
    financial_account.type = account_type
    financial_account.account = account
    financial_account.real_name = real_name
    financial_account.bank = bank
    financial_account.sub_bank = sub_bank
    financial_account.save()


def get_financial_accounts(user_id, show_brief_dct=False):
    financial_account = FinancialAccount.query.filter(FinancialAccount.user_id == user_id).all()
    if not show_brief_dct:
        return financial_account
    ret = {}
    for account in financial_account:
        ret[account.type] = mask_string(account.account)
    return ret


def get_financial_type_account_by_num(account_num, type):
    return FinancialAccount.query.filter(FinancialAccount.account == account_num,
                                         FinancialAccount.type == type).first()


def get_user_bankcard(user_id):
    return FinancialAccount.query.filter(FinancialAccount.user_id == user_id).filter(
        FinancialAccount.type == 'bank_card').first()


def delete_financial_account(user_id, account_type, admin_id, reason=''):
    financial_account = FinancialAccount.query.filter(FinancialAccount.user_id == user_id). \
        filter(FinancialAccount.type == account_type).first()
    if financial_account:
        financial_account.delete(auto_commit=False)
        info = {'type_account': financial_account.account, 'money': '0', 'reason': reason}
        if 'alipay' == account_type:
            info['type'] = WITHDRAW_BLACK_TYPE.BLACK_ALIPAYNO
            info['note'] = u'解绑支付宝'
        elif 'bank_card' == account_type:
            info['type'] = WITHDRAW_BLACK_TYPE.BLACK_BANK
            info['note'] = u'解绑银行卡'
        elif 'wechat' == account_type:
            info['type'] = WITHDRAW_BLACK_TYPE.BLACK_WECHAT
            info['note'] = u'解绑微信'
        info['reason'] = reason
        update_withdraw_black(info, admin_id, WITHDRAW_BLACK_STATUS.ENABLE)
        orm.session.commit()


def decrease_balance(user_id, amount, balance_type=BALANCE_TYPE.BALANCE):
    """
    减少用户账号余额
    :param user_id:
    :param amount: 扣除的钱，单位是分
    :param balance_type: BALANCE_TYPE
    :return: (decr_balance, decr_withdraw, decr_agent_balance) 返回三个余额分别实际扣减的金额， 单位是分
    """
    assert user_id > 0
    assert isinstance(amount, int)
    assert balance_type in [BALANCE_TYPE.BALANCE, BALANCE_TYPE.WITHDRAW]
    _LOGGER.info('user_id decrease account amount: %s %s %s' % (user_id, amount, balance_type))
    if amount <= 0:
        _LOGGER.info('user_id decrease less zero amount transaction: %s %s' % (user_id, amount))
        return
    account = Account.query.with_for_update().filter(Account.id == user_id).first()
    balance = convert_yuan_to_fen(account.balance)
    withdraw = convert_yuan_to_fen(account.withdraw)

    assert balance >= withdraw, 'user account balance data error: %s %s %s %s' % (
        user_id, balance, withdraw)

    if balance < amount:
        raise err.ResourceInsufficient(u'当前余额不足')
    if check_banker_lock(user_id):
        raise err.LockError('bull banker lock')
    if balance - amount < BULL_LOCK_RATIO * convert_yuan_to_fen(get_lock_amount(user_id)):
        raise err.BalanceInsufficient(u'账户余额不足')
    elif balance_type == BALANCE_TYPE.WITHDRAW:
        if withdraw < amount:
            raise err.ResourceInsufficient('withdraw balance not enough')
        balance -= amount
        withdraw = _decrease_from_out_to_in([withdraw], amount)
    elif balance_type == BALANCE_TYPE.BALANCE:
        balance, withdraw = _decrease_from_out_to_in([balance, withdraw], amount)
    account.balance = convert_fen_to_yuan(balance)
    account.withdraw = convert_fen_to_yuan(withdraw)
    account.save(auto_commit=False)
    orm.session.flush()
    _LOGGER.info('the current account: %s %s', account.balance, account.withdraw)
    return account


def _decrease_from_out_to_in(amount_list, decr):
    # 从amount_list里每一项扣钱，规则是优先扣取前面的，当前不够时，再扣取后面项的
    first = amount_list[0]
    result = [first - decr]
    for each in amount_list[1:]:
        diff = first - each
        result.append(each if decr <= diff else (first - decr))
    return result


@sql_wrapper
def get_financial_account(user_id, type):
    return FinancialAccount.query.filter(FinancialAccount.user_id == user_id,
                                         FinancialAccount.type == type).order_by(
        FinancialAccount.id.desc()).first()


def hide_phone_username(account):
    if account['phone'] and account['user_name']:
        if account['user_name'] == account['phone'] or account['user_name'] == account['phone'][2:]:
            account['user_name'] = mask_string(account['user_name'])
    if not account['phone'] and account['user_name']:
        if len(account['user_name']) == 11 and account['user_name'][0] in ['1', '8']:
            account['user_name'] = mask_string(account['user_name'])

    return account